package ade.prime.service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import ade.prime.activity.AdeWebNotification;
import ade.prime.activity.R;
import ade.prime.activity.adapter.ResourceFetcher;
import ade.prime.analyzer.ContainerData;
import ade.prime.analyzer.FeedAnalyzer;
import ade.prime.analyzer.FeedConnection;
import ade.prime.analyzer.FeedLesson;
import ade.prime.database.Database;
import ade.prime.exception.AAAException;
import ade.prime.model.AdeWebOptions;
import ade.prime.model.GuidScheduleUpdater;
import ade.prime.model.Lesson;
import ade.prime.model.LessonType;
import ade.prime.model.Lessons;
import ade.prime.model.Project;
import ade.prime.model.Resource;
import ade.prime.model.Schedule;
import ade.prime.model.ScheduleState;
import ade.prime.model.ScheduleUpdater;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

/**
 * @author		Schnell Michaël
 * @version		1.0
 */
public class AdeWebService extends Service
implements OnSharedPreferenceChangeListener {

	/**
	 * Used for logging purposes.
	 */
	protected static final String TAG = AdeWebService.class.getSimpleName() ;
	
	private static final String AUTO_UPDATE_TIMER_NAME = 
			"AutoUpdateTimer" ; //$NON-NLS-1$

	private static final long INITIAL_TIMER_DELAY = 1000L ;
	
	private static final long N_MILLIS_PER_HOURS = 60L * 60L * 1000L ;

	private static final long INITIAL_UPDATE_DELAY = 1000L ;

	/**
	 * Object responsible for periodically calling the update 
	 * of the schedules.
	 */
	protected Timer timer ;

	/**
	 * Callback doing the actual schedule update.
	 */
	protected TimerTask autoUpdateTask ;

	/**
	 * List of the listeners alerted when a schedule update occurred.
	 */
	protected List<AdeWebServiceListener> listeners ;

	/**
	 * API end point for this service.
	 */
	protected ApiEndPoint apiEndpoint ;
	
	/**
	 * Queue containing the schedules waiting for an update.
	 */
	protected List<Schedule> updateQueue ;
	
	/**
	 * Runnable used in a separate thread for updating the schedules in the
	 * update queue.
	 */
	protected ScheduledExecutorService scheduler ;

	/**
	 * {@inheritDoc}
	 */
	@Override
	public IBinder onBind(Intent intent) {
		if (AdeWebService.class.getName().equals(intent.getAction())) {
//			Log.d(TAG, "Bound by intent " + intent) ; //$NON-NLS-1$
			return this.apiEndpoint ;
		}// if
		return null ;
	}// onBind(Intent)

	@Override
	public void onCreate() {
		super.onCreate() ;
//		Log.i(TAG, "Service creating"); //$NON-NLS-1$
		this.listeners = new ArrayList<AdeWebServiceListener>() ;
		this.updateQueue = new ArrayList<Schedule>() ;
		this.apiEndpoint = new ApiEndPoint() ;
		this.scheduler = Executors.newScheduledThreadPool(1) ;

		if (AdeWebOptions.autoUpdateIsOn(this)) {
			this.startAutoUpdates() ;
		}// if
		
		// Add listener for preference changes
		AdeWebOptions.registerChangeListener(this, this) ;
	}// onCreate()

	@Override
	public void onDestroy() {
		super.onDestroy() ;
//		Log.i(TAG, "Service destroying") ; //$NON-NLS-1$
		
		// Remove listener for preference changes
		AdeWebOptions.unregisterChangeListener(this, this) ;

		if (this.timer != null) {
			this.cancelAutoUpdates() ;
		}// if
	}// onDestroy()
	
	protected void startAutoUpdates() {
		this.timer = new Timer(AUTO_UPDATE_TIMER_NAME) ;
		this.autoUpdateTask = new AutoUpdateTask() ;
		int nHours = AdeWebOptions.getnHoursBetweenUpdates(this) ;
		long updateDelay = nHours * N_MILLIS_PER_HOURS ;
		this.timer.schedule(this.autoUpdateTask, INITIAL_TIMER_DELAY, updateDelay) ;
	}// startAutoUpdates()
	
	protected void cancelAutoUpdates() {
		this.timer.cancel() ;
		this.timer = null ;
		this.autoUpdateTask = null ;
	}// cancelAutoUpdates()

	/**
	 * Callback for the timer doing the actual schedule update.
	 * 
	 * @author		Schnell Michaël
	 * @version		1.0
	 */
	private class AutoUpdateTask extends TimerTask {

		/**
		 * Sole constructor.
		 */
		protected AutoUpdateTask() {
			super() ;
		}// SchedulUpdate()

		/**
		 * Called by the timer, updates the schedules.
		 */
		@Override
		public void run() {
			AdeWebService service = AdeWebService.this ;
			synchronized (service.updateQueue) {
				try {
					for(Schedule schedule : service.apiEndpoint.getAllSchedules()) {
						service.apiEndpoint.updateSchedule(schedule) ;
					}// for
				} catch (RemoteException re) {
					Log.d(AdeWebService.TAG, 
							"Failed to auto-update all schedules.", re) ; //$NON-NLS-1$
				}// try
			}// synchronized
		}// run

	}// class AutoUpdateTask

	private class ApiEndPoint extends AdeWebServiceApi.Stub {
		
		protected int nextListenerId ;
		
		private AdeWebService service ;
		
		protected final Object databaseWriteLock ;
		
		/**
		 * 
		 */
		public ApiEndPoint() {
			super() ;
			this.service = AdeWebService.this ;
			this.databaseWriteLock = new Object() ;
			this.nextListenerId = 0 ;
		}// ApiEndPoint()

		@Override
		public int addListener(AdeWebServiceListener listener)
				throws RemoteException {
			synchronized (this.service.listeners) {
				this.service.listeners.add(listener) ;
			}// synchronized
			return this.nextListenerId++ ;
		}// addListener(AdeWebServiceListener)

		@Override
		public void removeListener(AdeWebServiceListener listener)
				throws RemoteException {
			synchronized (this.service.listeners) {
				Iterator<AdeWebServiceListener> iter = 
						this.service.listeners.iterator() ;
				int removedIndex = -1, currentIndex = 0 ;
				while (iter.hasNext() && (removedIndex == -1)) {
					AdeWebServiceListener next = iter.next() ;
					if (next.equals(listener)) {
						removedIndex = currentIndex ;
					}// if
					currentIndex++ ;
				}// while
				if (removedIndex != -1) {
					this.service.listeners.remove(removedIndex) ;
				}// if
			}// synchronized
		}// removeListener(AdeWebServiceListener)

		@Override
		public Schedule createSchedule(String name, Resource[] resources,
				int period, Project project) throws RemoteException {
//			Log.d(TAG, "Service - Creating schedule, values:") ; //$NON-NLS-1$
//			Log.d(TAG, "-- name: '" + name + "'") ; //$NON-NLS-1$ //$NON-NLS-2$
//			Log.d(TAG, "-- resources: " + Arrays.toString(resources)) ; //$NON-NLS-1$
//			Log.d(TAG, "-- period: " + period) ; //$NON-NLS-1$
//			Log.d(TAG, "-- project: " + project) ; //$NON-NLS-1$
			Schedule schedule = new Schedule(name, resources, period, project) ;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				try {
					schedule.validate(db) ;
					db.insertSchedule(schedule) ;
				} catch (AAAException ae) {
					Log.d(TAG, "-- Failed validate.", ae) ; //$NON-NLS-1$
					schedule = null ;
				}// try
			}// synchronized
//			Log.d(TAG, "-- Schedule -" + schedule) ; //$NON-NLS-1$
			if (schedule != null) {
				synchronized (this.service.listeners) {
					for(AdeWebServiceListener listener : this.service.listeners) {
						listener.handleNewSchedule(schedule) ;
					}// for
				}// synchronized
			}// if
			return schedule ;
		}// createSchedule(String, Resource[],int, Project)

		@Override
		public Schedule editSchedule(Schedule oldSchedule, String name,
				Resource[] resources, int period, Project project)
				throws RemoteException {
//			Log.d(TAG, "Service - Modifying schedule, values:") ; //$NON-NLS-1$
//			Log.d(TAG, "-- name: '" + name + "'") ; //$NON-NLS-1$ //$NON-NLS-2$
//			Log.d(TAG, "-- resources: " + Arrays.toString(resources)) ; //$NON-NLS-1$
//			Log.d(TAG, "-- period: " + period) ; //$NON-NLS-1$
//			Log.d(TAG, "-- project: " + project) ; //$NON-NLS-1$
			Schedule schedule = new Schedule(oldSchedule) ;
			schedule.setName(name) ;
			schedule.setResources(resources) ;
			schedule.setPeriod(period) ;
			schedule.setProject(project) ;
			if (!oldSchedule.equals(schedule)) {
				String oldName = oldSchedule.getName() ;
				synchronized (this.databaseWriteLock) {
					Database db = new Database(this.service) ;
					try {
						if (oldName.equals(name)) {
							schedule.validateOnUpdate() ;
						} else {
							schedule.validate(db) ;
						}// if
						db.updateSchedule(oldSchedule, schedule) ;
					} catch (AAAException ae) {
						Log.d(TAG, "-- Failed validate.", ae) ; //$NON-NLS-1$
						schedule = null ;
					}// try
				}// synchronized
//				Log.d(TAG, "-- Schedule -" + schedule) ; //$NON-NLS-1$
				if (schedule != null) {
					synchronized (this.service.listeners) {
						for(AdeWebServiceListener listener : this.service.listeners) {
							listener.handleModifiedSchedule(schedule) ;
						}// for
					}// synchronized
				}// if
			}// if
			return schedule ;
		}// editSchedule(Schedule, String, Resource[], int, Project)

		@Override
		public void removeSchedule(Schedule schedule) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				db.removeSchedule(schedule.getId()) ;
			}// synchronized
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					listener.handleRemovedSchedule(schedule) ;
				}// for
			}// synchronized
		}// removeSchedule(Schedule)

		@Override
		public void updateSchedule(Schedule schedule) throws RemoteException {
			Schedule addedSchedule = null ;
			synchronized (this.service.updateQueue) {
				if (!this.service.updateQueue.contains(schedule)) {
					addedSchedule = new Schedule(schedule) ;
					this.service.updateQueue.add(addedSchedule) ;
					this.service.startScheduleUpdateTask() ;
				}// if
			}// synchronized
			if (addedSchedule != null) {
				this.updateScheduleState(addedSchedule, 
						ScheduleState.WAITING_FOR_UPDATE, null) ;
			}// if
		}// updateSchedule(Schedule)
		
		protected void updateScheduleState(Schedule schedule, ScheduleState newState,
				ScheduleUpdateProgress newUpdateState) {
//			Log.d(TAG, String.format("[updateScheduleState]: %s (%s) - %s.%s",
//					schedule.getName(), Integer.valueOf(schedule.getId()),
//					newState, newUpdateState)) ;
			if (!schedule.getState().equals(newState)) {
				schedule.setState(newState) ;
				synchronized (this.databaseWriteLock) {
					Database db = new Database(this.service) ;
					db.updateScheduleState(schedule.getId(), newState) ;
				}// synchronized
			}// if
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					try {
						listener.handleModifiedScheduleState(schedule, newUpdateState) ;
					} catch (RemoteException e) {
						// TODO Handle
						Log.d(TAG, "Failed to notify of schedule state change", e) ; //$NON-NLS-1$
					}// try
				}// for
			}// synchronized
		}// updateScheduleState(Schedule, ScheduleState)

		@Override
		public void cancelScheduleUpdate(Schedule schedule)
				throws RemoteException {
			boolean stateChange = false ;
			if (!schedule.getState().equals(ScheduleState.UPDATING)) {
				synchronized (this.service.updateQueue) {
					if (this.service.updateQueue.contains(schedule)) {
						this.service.updateQueue.remove(schedule) ;
						stateChange = true ;
					}// if
				}// synchronized
				if (stateChange) {
					this.updateScheduleState(schedule, ScheduleState.NOT_UPDATED, null) ;
				}// if
			}// if
		}// cancelScheduleUpdate(Schedule)

		@Override
		public List<Schedule> getAllSchedules() throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getAllSchedules() ;
			}// synchronized
		}// getAllSchedules()

		@Override
		public ScheduleState getScheduleState(Schedule schedule)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getSchedule(schedule.getId()).getState() ;
			}// synchronized
		}// getScheduleState(Schedule)

		@Override
		public Lesson getLesson(long lessonId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getLesson(lessonId) ;
			}// synchronized
		}// getLesson(long)

		@Override
		public LessonType getLessonType(Lesson lesson) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getLesson(lesson.getLessonId()).getType() ;
			}// synchronized
		}// getLessonType(Lesson)

		@Override
		public Lesson setLessonType(Lesson lesson, LessonType type)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				lesson.setType(type) ;
				db.updateLesson(lesson.getLessonId(), lesson) ;
			}// synchronized
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					listener.handleModifiedLessonType(lesson) ;
				}// for
			}// synchronized			
			return lesson ;
		}// setLessonType(Lesson lessen, LessonType)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Schedule getSchedule(int scheduleId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getSchedule(scheduleId) ;
			}// synchronized
		}// getSchedule(int)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Lessons getAllScheduleLessons(int scheduleId)
				throws RemoteException {
			Lesson[] lessons ;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				lessons = db.getAllScheduleLessons(scheduleId) ;
			}// synchronized
			return new Lessons(lessons) ;
		}// getAllScheduleLessons(String)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Lessons getFutureScheduleLessons(int scheduleId)
				throws RemoteException {
			Lesson[] lessons ;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				lessons = db.getFutureScheduleLessons(scheduleId) ;
			}// synchronized
			return new Lessons(lessons) ;
		}// getFutureScheduleLessons(String)

	}// class ApiEndPoint()
	
	protected void startScheduleUpdateTask() {
		this.scheduler.schedule(new ScheduleUpdateRunnable(), 
				INITIAL_UPDATE_DELAY, TimeUnit.MILLISECONDS) ;
	}// startScheduleUpdateTask()
	
	private class ScheduleUpdateRunnable implements Runnable {
		protected ScheduleUpdateRunnable() {
			super() ;
		}// ScheduleUpdateRunnable()
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void run() {
			AdeWebService.this.updateAllSchedules(true) ;
		}// run()
	}// class ScheduleUpdateRunnable
	
	protected void updateAllSchedules(boolean keepOldScheduleState) {
		AdeWebNotification.initScheduleNotificationNumber() ;
		while (!this.updateQueue.isEmpty()) {
			
			Schedule schedule ;
			synchronized (this.updateQueue) {
				schedule = this.updateQueue.get(0) ;
			}// synchronized

			//TODO Retrieve options from schedule
			boolean notify = true ;
			
			this.updateSchedule(schedule, keepOldScheduleState, notify) ;
			
			synchronized (this.updateQueue) {
//				Log.d(TAG, "[Remove] Before: " + this.updateQueue.size()) ;
				this.updateQueue.remove(0) ;
//				Log.d(TAG, "[Remove] After : " + this.updateQueue.size()) ;
			}// synchronized
			
		}// while
		
//		synchronized (this.updateQueue) {
//			Iterator<Schedule> iterator = this.updateQueue.iterator() ;
//			while (iterator.hasNext()) {
//				Schedule schedule = iterator.next() ;
//				this.updateSchedule(schedule) ;
//				iterator.remove() ;
//			}// if
//		}// synchronized
	}// updateAllSchedules()
	
	protected void updateSchedule(Schedule schedule, boolean keepOldScheduleState,
			boolean notify) {
		ScheduleState failureState = ScheduleState.NOT_UPDATED ;
		if (keepOldScheduleState) {
			failureState = schedule.getState() ;
		}// if
//		try {
//			this.apiEndpoint.updateScheduleState(schedule, 
//					ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_1) ;
//			Thread.sleep(1000) ;
//			this.apiEndpoint.updateScheduleState(schedule, 
//					ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_2) ;
//			Thread.sleep(1000) ;
//			this.apiEndpoint.updateScheduleState(schedule, 
//					ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_3) ;
//			Thread.sleep(1000) ;
//			this.apiEndpoint.updateScheduleState(schedule, 
//					ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_4) ;
//			Thread.sleep(1000) ;
//			this.apiEndpoint.updateScheduleState(schedule, 
//					ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_5) ;
//			Thread.sleep(1000) ;
//			this.apiEndpoint.updateScheduleState(schedule, 
//					ScheduleState.UPDATED, ScheduleUpdateProgress.STEP_6) ;
//		} catch (InterruptedException e) {
//			// TODO Auto-generated catch block
//			e.printStackTrace() ;
//		}// try
		try {
			// Main steps in each update (each step is followed by an update)
			this.connectToAdeApi(schedule) ;
			this.setProject(schedule) ;
			this.getLessonsFeed(schedule) ;
			this.analyzeLessonsFeed(schedule) ;
			Database db = new Database(this) ;
			synchronized (this.apiEndpoint.databaseWriteLock) {
				this.compareWithPreviousSchedule(schedule, db) ;
				this.updateScheduleInsideDatabase(schedule, db) ;
			}// synchronized
			this.apiEndpoint.updateScheduleState(schedule, 
					ScheduleState.UPDATED, null) ;
			if (notify) {
				// Check if any lesson planned within the next two days was modified
				if (this.scheduleDiff.importantChange()) {
					AdeWebNotification.makeScheduleNotification(this, schedule) ;
				}// if
			}// if
		} catch (AAAException ae) {
			Log.d(TAG, String.format("Failed to update schedule %s (%d)", //$NON-NLS-1$
					schedule.getName(), Integer.valueOf(schedule.getId())), ae) ;
			this.apiEndpoint.updateScheduleState(schedule, 
					failureState, null) ;
		}// try
//		try {
//			ApiEndPoint api = (ApiEndPoint) this.apiEndpoint ;
//			Database db = new Database(this) ;
//
//			// On débute avec la mise à jour
//			api.updateScheduleState(schedule, ScheduleState.UPDATING) ; 
//
//			try {
//				Schedule newS = db.getSchedule(schedule.getId()) ;
//				newS.setStudentNumList() ;
//				newS.build() ;
//				Schedule oldS = schedule ;
//				oldS.buildComplete(db) ;
//				db.updateLastUpdateForSchedule(oldS.getId()) ;
//				ade.prime.model.ScheduleUpdater updater = 
//						new GuidScheduleUpdater() ;
//				updater.compare(oldS, newS, db) ;
//				updater.updateSchedule(db) ;
//
//				// Fin de la mise à jour
//				api.updateScheduleState(schedule, ScheduleState.UPDATED) ;
//			} catch (AAAException ae) {
//				// TODO: handle exception
//				// Echec de la mise à jour
//				api.updateScheduleState(schedule, ScheduleState.NOT_UPDATED) ;
//			}// try
//			
//		} catch (RemoteException re) {
//			// TODO: handle exception
//		}// try
	}// updateSchedule(Schedule)
	
	protected FeedConnection connection ;
	
	protected List<FeedLesson> lessonsFeed ;
	
	protected Schedule newSchedule ;
	
	protected ScheduleUpdater scheduleDiff ;
	
	protected void connectToAdeApi(Schedule schedule) throws AAAException {
		this.apiEndpoint.updateScheduleState(schedule, 
				ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_1) ;

		this.connection = ContainerData.getFeedConnexion() ;
		if (this.connection == null) {
			String errmsg = 
					this.getResources().getString(R.string.ADE_connection_failed) ;
			throw new AAAException(errmsg) ;
		}// if
	}// connectToAdeApi(Schedule)
	
	protected void setProject(Schedule schedule) throws AAAException {
		this.apiEndpoint.updateScheduleState(schedule, 
				ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_2) ;

		String sessionId = this.connection.getId() ;
//		this.connection = 
				ContainerData.getFeedSetProject(sessionId, 
				schedule.getProject().getAdeProjectId()) ;
	}// setProject(Schedule)
	
	protected void getLessonsFeed(Schedule schedule) throws AAAException {
		this.apiEndpoint.updateScheduleState(schedule, 
				ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_3) ;
		
		String sessionId = this.connection.getId() ;
		this.lessonsFeed = ContainerData.getFeedsLesson(sessionId, 
				schedule.getAllResources(), schedule.getPeriod()) ;
		// On a fini la récupération de données depuis ADE, on se déconnecte
		this.connection = ContainerData.getFeedDeconnexion(sessionId) ;
	}// getLessonsFeed()
	
	protected void analyzeLessonsFeed(Schedule schedule) throws AAAException {
		this.apiEndpoint.updateScheduleState(schedule, 
				ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_4) ;
		
		this.newSchedule = new Schedule() ;
		this.newSchedule.setId(schedule.getId()) ;
		FeedAnalyzer feedAnalyzer = 
				new FeedAnalyzer(this.newSchedule, this.lessonsFeed) ;
		feedAnalyzer.start() ;
	}// analyzeLessonsFeed()
	
	protected void compareWithPreviousSchedule(Schedule schedule,
			Database db) throws AAAException {
		this.apiEndpoint.updateScheduleState(schedule, 
				ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_5) ;
		
		schedule.buildComplete(db) ;
		this.scheduleDiff = new GuidScheduleUpdater() ;
		this.scheduleDiff.compare(schedule, this.newSchedule) ;
	}// compareWithPreviousSchedule()
	
	protected void updateScheduleInsideDatabase(Schedule schedule,
			Database db) throws AAAException {
		this.apiEndpoint.updateScheduleState(schedule, 
				ScheduleState.UPDATING, ScheduleUpdateProgress.STEP_6) ;

		this.scheduleDiff.updateSchedule(db) ;
	}// updateScheduleInsideDatabase()

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
			String key) {
		if (key.equals(ResourceFetcher.getString(this, R.string.Preferences_color_ADE))) {
			if (AdeWebOptions.autoUpdateIsOn(this)) {
				this.startAutoUpdates() ;
			} else {
				this.cancelAutoUpdates() ;
			}// if
		}// if
	}// onSharedPreferenceChanged(SharedPreferences, String)

}// class AdeWebService